Skip to content

Conversation

@jack-arturo
Copy link
Member

Introduces the 'evernote_patch_note' tool for applying targeted find-and-replace operations to note content without regenerating the entire note. Implements backend logic in EvernoteAPI, updates types for replacements and patch results, and adds comprehensive integration tests for various patching scenarios.

Introduces the 'evernote_patch_note' tool for applying targeted find-and-replace operations to note content without regenerating the entire note. Implements backend logic in EvernoteAPI, updates types for replacements and patch results, and adds comprehensive integration tests for various patching scenarios.
@jack-arturo jack-arturo changed the base branch from main to develop January 19, 2026 16:24
@coderabbitai
Copy link

coderabbitai bot commented Jan 19, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

📝 Walkthrough

Walkthrough

Adds an Evernote "patch note" tool and API to perform ordered find-and-replace operations on note content, new types for replacements/results, updates to markdown↔ENML handling and resource preservation, and extensive integration tests (including a duplicated test block).

Changes

Cohort / File(s) Summary
Type Definitions
src/types.ts
Adds NoteReplacement and PatchNoteResult interfaces for patch operations and results.
API Implementation
src/evernote-api.ts
Adds patchNoteContent(guid, replacements) and updates applyMarkdownToNote(..., options) to support preserving resources; changes ENML↔Markdown conversion order and adds per-replacement tracking, validation (abort on no changes or empty result), and persistence logic.
Tool Integration
src/index.ts
Registers evernote_patch_note tool with input schema, validates replacements, invokes evernoteApi.patchNoteContent, and returns formatted success/failure summaries.
Integration Tests
__tests__/integration/mcp-tools.test.ts
Adds a comprehensive "Patch Note Operations" test suite covering single/multiple replacements, replaceAll semantics, no-match/no-replacements errors, empty-result rejection, resource preservation, note-not-found and API error propagation — note: the suite appears duplicated within the file (two identical describe blocks).
Unit Tests / ENML parsing
__tests__/unit/search-preview.test.ts
Reorders HTML entity decoding so & is decoded after other entities (comment added); behavior note may affect entity handling order.

Sequence Diagram

sequenceDiagram
    participant Client
    participant MCPServer as MCP Server
    participant EvernoteAPI
    participant EvernoteService as Evernote Service

    Client->>MCPServer: Call evernote_patch_note(guid, replacements)
    MCPServer->>MCPServer: Validate input (replacements present, non-empty find)
    MCPServer->>EvernoteAPI: patchNoteContent(guid, replacements)
    EvernoteAPI->>EvernoteService: getNote(guid, includeResources)
    EvernoteService-->>EvernoteAPI: Note with ENML & resources
    EvernoteAPI->>EvernoteAPI: Convert ENML to Markdown
    EvernoteAPI->>EvernoteAPI: Apply replacements sequentially (track occurrences/replaced)
    EvernoteAPI->>EvernoteAPI: Validate result (changes occurred, not empty)
    EvernoteAPI->>EvernoteAPI: Convert Markdown back to ENML (preserve resources)
    EvernoteAPI->>EvernoteService: updateNote(updatedNote)
    EvernoteService-->>EvernoteAPI: Update confirmation
    EvernoteAPI-->>MCPServer: PatchNoteResult(success, changes[], optional warning)
    MCPServer-->>Client: Human-readable success/failure summary with details
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding a new patch tool for targeted find-and-replace operations on Evernote notes, which aligns with the primary objective of the changeset.
Description check ✅ Passed The description directly relates to the changeset by explaining the new evernote_patch_note tool, backend implementation, type updates, and integration tests—all reflected in the file changes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/patch-note

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
__tests__/integration/mcp-tools.test.ts (1)

82-82: Test assertion is outdated and will fail with the actual tool count.

The expect(result.tools).toHaveLength(11) assertion doesn't match the actual tools registered. The listToolsHandler in src/index.ts returns the complete tools array containing all 26 tools (including the new evernote_patch_note), not just 11. Update the test to:

  1. Change the expected length from 11 to 26
  2. Add evernote_patch_note to the tools being checked
🤖 Fix all issues with AI agents
In `@src/evernote-api.ts`:
- Around line 134-170: patchNoteContent currently calls applyMarkdownToNote
after replacements which causes unreferenced resources to be dropped; update
patchNoteContent to preserve existing note.resources when applying the patched
markdown (or modify applyMarkdownToNote to merge resources) so attachments not
referenced in the new markdown are retained. Specifically, in patchNoteContent
(after getNote and convertENMLToMarkdown) ensure you pass the original
note.resources (or merge them inside applyMarkdownToNote) instead of letting
applyMarkdownToNote unconditionally replace resources; mirror the behavior used
by createNote which preserves and adds to existing resources. Make sure getNote,
convertENMLToMarkdown, and applyMarkdownToNote cooperate to avoid deleting
attachments that weren't explicitly removed by the replacement.
🧹 Nitpick comments (1)
src/index.ts (1)

1389-1422: Consider validating empty find strings.

The handler validates that replacements is a non-empty array, but doesn't check for empty find strings. An empty find string would match at every position in the content, leading to unexpected behavior:

"abc".split("").join("X")  // Returns "XaXbXcX"
🔧 Suggested validation enhancement
 case 'evernote_patch_note': {
   const { guid, replacements } = args as any;

   if (!replacements || !Array.isArray(replacements) || replacements.length === 0) {
     throw new Error('At least one replacement must be provided');
   }

+  for (const r of replacements) {
+    if (!r.find || typeof r.find !== 'string' || r.find.length === 0) {
+      throw new Error('Each replacement must have a non-empty "find" string');
+    }
+  }

   const result = await evernoteApi.patchNoteContent(guid, replacements);

jack-arturo and others added 4 commits January 21, 2026 01:58
- Preserve existing resources during patch operation by saving
  original resources and merging them back after markdown conversion
- Add validation for empty find strings to prevent unexpected behavior
- Update test assertion for tool count from 11 to 28
- Add evernote_patch_note to tested tool names
- Add tests for empty find string validation and resource preservation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…Note

Instead of post-hoc restoration after calling applyMarkdownToNote,
add a preserveResources option that handles the merge internally.
This is cleaner and follows the single-responsibility principle.

- Add optional { preserveResources: boolean } parameter to applyMarkdownToNote
- When enabled, merges original resources with new attachments
- patchNoteContent now simply passes { preserveResources: true }
- Maintains backward compatibility (default behavior unchanged)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Move &amp; decoding to last position when unescaping HTML entities.
This prevents security issues where &amp;lt; would incorrectly become <
instead of &lt;

Fixes CodeQL alert #7 (js/double-escaping)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@jack-arturo
Copy link
Member Author

@CodeRabbit review

@coderabbitai
Copy link

coderabbitai bot commented Jan 21, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/evernote-api.ts`:
- Around line 198-257: The patchNoteContent flow should validate inputs before
performing I/O: check that the replacements array (parameter replacements:
NoteReplacement[]) is non-empty and that each replacement has a non-empty find
string (and optionally a replace value) before calling getNote(guid, true,
true); if validation fails return the same shape PatchNoteResult with
success:false, noteGuid:guid, an empty or appropriate changes array and a
warning message (e.g., "No replacements provided" or "Empty find string in
replacements") instead of fetching the note; implement this logic at the start
of patchNoteContent to prevent runaway/expensive operations and reference the
function name patchNoteContent and the types NoteReplacement and PatchNoteResult
when making the change.

Comment on lines 198 to 257
async patchNoteContent(guid: string, replacements: NoteReplacement[]): Promise<PatchNoteResult> {
// Fetch existing note with content and resources
const note = await this.getNote(guid, true, true);

// Convert ENML to markdown
let markdown = this.convertENMLToMarkdown(note.content, note.resources);

// Track changes
const changes: PatchNoteResult['changes'] = [];

// Apply replacements sequentially
for (const replacement of replacements) {
const { find, replace, replaceAll = true } = replacement;

// Count occurrences
const regex = new RegExp(find.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g');
const matches = markdown.match(regex);
const occurrences = matches ? matches.length : 0;

// Perform replacement
let replaced = 0;
if (occurrences > 0) {
if (replaceAll) {
markdown = markdown.split(find).join(replace);
replaced = occurrences;
} else {
markdown = markdown.replace(find, replace);
replaced = 1;
}
}

changes.push({
find,
occurrences,
replaced,
});
}

// Check if any changes were made
const totalReplaced = changes.reduce((sum, c) => sum + c.replaced, 0);
if (totalReplaced === 0) {
return {
success: false,
noteGuid: guid,
changes,
warning: 'No matches found for any replacement patterns',
};
}

// Check if content would be empty after replacement
const trimmedMarkdown = markdown.trim();
if (!trimmedMarkdown) {
return {
success: false,
noteGuid: guid,
changes,
warning: 'Replacement would result in empty note content - operation aborted',
};
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find the NoteReplacement type definition
rg "interface NoteReplacement|type NoteReplacement" -A 5 src/

Repository: verygoodplugins/mcp-evernote

Length of output: 246


🏁 Script executed:

# Check the full patchNoteContent implementation for context
cat -n src/evernote-api.ts | sed -n '198,260p'

Repository: verygoodplugins/mcp-evernote

Length of output: 2446


🏁 Script executed:

# Look for any existing validation in the file
rg "throw new Error|if.*replacements" src/evernote-api.ts -B 2 -A 2

Repository: verygoodplugins/mcp-evernote

Length of output: 54


🏁 Script executed:

# Check if there are type guards or runtime checks already
rg "PatchNoteResult|NoteReplacement" src/ -B 1 -A 3

Repository: verygoodplugins/mcp-evernote

Length of output: 1250


Add defensive validation for empty replacements and find strings before I/O.

An empty find string creates a regex matching every character boundary, causing runaway replacements and performance issues. An empty replacements array still triggers an unnecessary note fetch. Validate both inputs before calling getNote().

Suggested fix
 async patchNoteContent(guid: string, replacements: NoteReplacement[]): Promise<PatchNoteResult> {
+  if (!Array.isArray(replacements) || replacements.length === 0) {
+    throw new Error('At least one replacement must be provided');
+  }
+  for (const { find } of replacements) {
+    if (!find) {
+      throw new Error('Each replacement must have a non-empty "find" string');
+    }
+  }
   // Fetch existing note with content and resources
   const note = await this.getNote(guid, true, true);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async patchNoteContent(guid: string, replacements: NoteReplacement[]): Promise<PatchNoteResult> {
// Fetch existing note with content and resources
const note = await this.getNote(guid, true, true);
// Convert ENML to markdown
let markdown = this.convertENMLToMarkdown(note.content, note.resources);
// Track changes
const changes: PatchNoteResult['changes'] = [];
// Apply replacements sequentially
for (const replacement of replacements) {
const { find, replace, replaceAll = true } = replacement;
// Count occurrences
const regex = new RegExp(find.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g');
const matches = markdown.match(regex);
const occurrences = matches ? matches.length : 0;
// Perform replacement
let replaced = 0;
if (occurrences > 0) {
if (replaceAll) {
markdown = markdown.split(find).join(replace);
replaced = occurrences;
} else {
markdown = markdown.replace(find, replace);
replaced = 1;
}
}
changes.push({
find,
occurrences,
replaced,
});
}
// Check if any changes were made
const totalReplaced = changes.reduce((sum, c) => sum + c.replaced, 0);
if (totalReplaced === 0) {
return {
success: false,
noteGuid: guid,
changes,
warning: 'No matches found for any replacement patterns',
};
}
// Check if content would be empty after replacement
const trimmedMarkdown = markdown.trim();
if (!trimmedMarkdown) {
return {
success: false,
noteGuid: guid,
changes,
warning: 'Replacement would result in empty note content - operation aborted',
};
}
async patchNoteContent(guid: string, replacements: NoteReplacement[]): Promise<PatchNoteResult> {
if (!Array.isArray(replacements) || replacements.length === 0) {
throw new Error('At least one replacement must be provided');
}
for (const { find } of replacements) {
if (!find) {
throw new Error('Each replacement must have a non-empty "find" string');
}
}
// Fetch existing note with content and resources
const note = await this.getNote(guid, true, true);
// Convert ENML to markdown
let markdown = this.convertENMLToMarkdown(note.content, note.resources);
// Track changes
const changes: PatchNoteResult['changes'] = [];
// Apply replacements sequentially
for (const replacement of replacements) {
const { find, replace, replaceAll = true } = replacement;
// Count occurrences
const regex = new RegExp(find.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g');
const matches = markdown.match(regex);
const occurrences = matches ? matches.length : 0;
// Perform replacement
let replaced = 0;
if (occurrences > 0) {
if (replaceAll) {
markdown = markdown.split(find).join(replace);
replaced = occurrences;
} else {
markdown = markdown.replace(find, replace);
replaced = 1;
}
}
changes.push({
find,
occurrences,
replaced,
});
}
// Check if any changes were made
const totalReplaced = changes.reduce((sum, c) => sum + c.replaced, 0);
if (totalReplaced === 0) {
return {
success: false,
noteGuid: guid,
changes,
warning: 'No matches found for any replacement patterns',
};
}
// Check if content would be empty after replacement
const trimmedMarkdown = markdown.trim();
if (!trimmedMarkdown) {
return {
success: false,
noteGuid: guid,
changes,
warning: 'Replacement would result in empty note content - operation aborted',
};
}
🤖 Prompt for AI Agents
In `@src/evernote-api.ts` around lines 198 - 257, The patchNoteContent flow should
validate inputs before performing I/O: check that the replacements array
(parameter replacements: NoteReplacement[]) is non-empty and that each
replacement has a non-empty find string (and optionally a replace value) before
calling getNote(guid, true, true); if validation fails return the same shape
PatchNoteResult with success:false, noteGuid:guid, an empty or appropriate
changes array and a warning message (e.g., "No replacements provided" or "Empty
find string in replacements") instead of fetching the note; implement this logic
at the start of patchNoteContent to prevent runaway/expensive operations and
reference the function name patchNoteContent and the types NoteReplacement and
PatchNoteResult when making the change.

Add early validation in patchNoteContent to check replacements array
before calling getNote. Returns PatchNoteResult with success:false
and appropriate warning instead of performing expensive I/O operations.

Validates:
- replacements array is non-empty
- each NoteReplacement has a non-empty find string

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@jack-arturo jack-arturo merged commit e7030b3 into develop Jan 21, 2026
16 checks passed
@jack-arturo jack-arturo deleted the feat/patch-note branch January 21, 2026 02:17
@jack-arturo jack-arturo linked an issue Jan 21, 2026 that may be closed by this pull request
@coderabbitai coderabbitai bot mentioned this pull request Jan 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Partial updates (?)

2 participants